Trading Domain

Rapid overview

Trading Domain Practice Exercises

Master forex trading concepts, MT4/MT5 integration, market data processing, and risk management for financial trading systems.

---

Foundational Questions

Q: Describe the lifecycle of a forex trade from placement to settlement.

A: Steps: quote, order placement, validation, routing, execution (fill/partial), confirmation, settlement (T+2), P&L updates. Post-trade, apply trade capture in back-office systems and reconcile with liquidity providers. Include margin checks and clearing, corporate actions, and overnight financing (swap) adjustments.

Q: How would you integrate with MT4/MT5 APIs for trade execution in C#? Mention authentication, session management, and error handling.

A: Use MetaTrader Manager/Server APIs via C# wrappers; handle session auth, keep-alive, throttle requests. Manage connections via dedicated service accounts and pre-allocate connection pools. Implement reconnect logic, map errors, ensure idempotent order submission. Translate MT-specific error codes into domain-level responses for clients.

using var session = new Mt5Gateway(credentials);
await session.ConnectAsync();
var ticket = await session.SendOrderAsync(request);

Q: What are common risk checks before executing a client order (e.g., margin, exposure limits)?

A: Margin availability, max exposure per instrument, credit limits, duplicate orders, fat-finger (price deviation). Implement pre-trade risk service.

Q: Explain how you'd handle market data bursts without dropping updates.

A: Use batching, diff updates, UDP multicast ingestion, prioritized queues, snapshot + incremental updates. Utilize adaptive sampling—send every tick to VIP clients while throttling retail feeds. Apply throttling per client, drop non-critical updates after stale, and monitor queue depths to trigger auto-scaling.

---

Trade Lifecycle Management

Q: Implement order validation pipeline with multiple risk checks.

A: Chain validation steps before order execution.

public interface IOrderValidator
{
    Task<ValidationResult> ValidateAsync(Order order, CancellationToken ct = default);
}

public class MarginValidator : IOrderValidator
{
    private readonly IAccountRepository _accountRepo;
    private readonly IMarginCalculator _marginCalc;

    public async Task<ValidationResult> ValidateAsync(Order order, CancellationToken ct)
    {
        var account = await _accountRepo.GetByIdAsync(order.AccountId, ct);

        var requiredMargin = _marginCalc.CalculateRequiredMargin(
            order.Symbol,
            order.Volume,
            order.Leverage);

        var availableMargin = account.Equity - account.UsedMargin;

        if (requiredMargin > availableMargin)
        {
            return ValidationResult.Failure(
                "INSUFFICIENT_MARGIN",
                $"Required: {requiredMargin:F2}, Available: {availableMargin:F2}");
        }

        return ValidationResult.Success();
    }
}

public class ExposureLimitValidator : IOrderValidator
{
    private readonly IPositionRepository _positionRepo;
    private readonly IRiskConfigService _riskConfig;

    public async Task<ValidationResult> ValidateAsync(Order order, CancellationToken ct)
    {
        var config = await _riskConfig.GetConfigAsync(order.AccountId, ct);
        var currentPositions = await _positionRepo.GetByAccountAsync(order.AccountId, ct);

        var symbolExposure = currentPositions
            .Where(p => p.Symbol == order.Symbol)
            .Sum(p => p.Volume);

        var newExposure = order.Type == OrderType.Buy
            ? symbolExposure + order.Volume
            : symbolExposure - order.Volume;

        if (Math.Abs(newExposure) > config.MaxExposurePerSymbol)
        {
            return ValidationResult.Failure(
                "EXPOSURE_LIMIT_EXCEEDED",
                $"Max exposure: {config.MaxExposurePerSymbol}, New exposure: {Math.Abs(newExposure)}");
        }

        return ValidationResult.Success();
    }
}

public class PriceDeviationValidator : IOrderValidator
{
    private readonly IMarketDataService _marketData;
    private const decimal MaxDeviationPercent = 2.0m;

    public async Task<ValidationResult> ValidateAsync(Order order, CancellationToken ct)
    {
        if (order.Type != OrderType.Limit && order.Type != OrderType.Stop)
            return ValidationResult.Success();

        var currentPrice = await _marketData.GetPriceAsync(order.Symbol, ct);
        var referencePrice = order.Side == OrderSide.Buy ? currentPrice.Ask : currentPrice.Bid;

        var deviation = Math.Abs((order.Price - referencePrice) / referencePrice) * 100;

        if (deviation > MaxDeviationPercent)
        {
            return ValidationResult.Failure(
                "PRICE_DEVIATION",
                $"Order price {order.Price} deviates {deviation:F2}% from market {referencePrice}");
        }

        return ValidationResult.Success();
    }
}

// Composite validator pipeline
public class OrderValidationPipeline
{
    private readonly IEnumerable<IOrderValidator> _validators;
    private readonly ILogger<OrderValidationPipeline> _logger;

    public OrderValidationPipeline(
        IEnumerable<IOrderValidator> validators,
        ILogger<OrderValidationPipeline> logger)
    {
        _validators = validators;
        _logger = logger;
    }

    public async Task<ValidationResult> ValidateAsync(Order order, CancellationToken ct = default)
    {
        foreach (var validator in _validators)
        {
            var result = await validator.ValidateAsync(order, ct);
            if (!result.IsValid)
            {
                _logger.LogWarning(
                    "Order {OrderId} failed validation: {ValidationError}",
                    order.Id,
                    result.ErrorCode);
                return result;
            }
        }

        return ValidationResult.Success();
    }
}

// DI registration
services.AddScoped<IOrderValidator, MarginValidator>();
services.AddScoped<IOrderValidator, ExposureLimitValidator>();
services.AddScoped<IOrderValidator, PriceDeviationValidator>();
services.AddScoped<IOrderValidator, DuplicateOrderValidator>();
services.AddScoped<OrderValidationPipeline>();

Q: Design state machine for order lifecycle tracking.

A: Implement order state transitions with guards.

public enum OrderState
{
    Pending,
    Validated,
    Submitted,
    PartiallyFilled,
    Filled,
    Cancelled,
    Rejected,
    Expired
}

public enum OrderEvent
{
    Validate,
    Submit,
    PartialFill,
    Fill,
    Cancel,
    Reject,
    Expire
}

public class OrderStateMachine
{
    private static readonly Dictionary<(OrderState, OrderEvent), OrderState> _transitions = new()
    {
        { (OrderState.Pending, OrderEvent.Validate), OrderState.Validated },
        { (OrderState.Pending, OrderEvent.Reject), OrderState.Rejected },

        { (OrderState.Validated, OrderEvent.Submit), OrderState.Submitted },
        { (OrderState.Validated, OrderEvent.Cancel), OrderState.Cancelled },

        { (OrderState.Submitted, OrderEvent.PartialFill), OrderState.PartiallyFilled },
        { (OrderState.Submitted, OrderEvent.Fill), OrderState.Filled },
        { (OrderState.Submitted, OrderEvent.Cancel), OrderState.Cancelled },
        { (OrderState.Submitted, OrderEvent.Reject), OrderState.Rejected },
        { (OrderState.Submitted, OrderEvent.Expire), OrderState.Expired },

        { (OrderState.PartiallyFilled, OrderEvent.PartialFill), OrderState.PartiallyFilled },
        { (OrderState.PartiallyFilled, OrderEvent.Fill), OrderState.Filled },
        { (OrderState.PartiallyFilled, OrderEvent.Cancel), OrderState.Cancelled },
        { (OrderState.PartiallyFilled, OrderEvent.Expire), OrderState.Expired }
    };

    private readonly Order _order;
    private readonly IEventPublisher _eventPublisher;
    private readonly ILogger<OrderStateMachine> _logger;

    public OrderStateMachine(
        Order order,
        IEventPublisher eventPublisher,
        ILogger<OrderStateMachine> logger)
    {
        _order = order;
        _eventPublisher = eventPublisher;
        _logger = logger;
    }

    public async Task<bool> TransitionAsync(OrderEvent @event)
    {
        var currentState = _order.State;

        if (!_transitions.TryGetValue((currentState, @event), out var newState))
        {
            _logger.LogWarning(
                "Invalid transition: Order {OrderId} cannot transition from {CurrentState} with event {@Event}",
                _order.Id,
                currentState,
                @event);
            return false;
        }

        _order.State = newState;
        _order.UpdatedAt = DateTime.UtcNow;

        await _eventPublisher.PublishAsync(new OrderStateChangedEvent
        {
            OrderId = _order.Id,
            PreviousState = currentState,
            NewState = newState,
            Event = @event,
            Timestamp = _order.UpdatedAt
        });

        _logger.LogInformation(
            "Order {OrderId} transitioned from {PreviousState} to {NewState} via {@Event}",
            _order.Id,
            currentState,
            newState,
            @event);

        return true;
    }

    public bool CanTransition(OrderEvent @event)
    {
        return _transitions.ContainsKey((_order.State, @event));
    }

    public IEnumerable<OrderEvent> GetAllowedEvents()
    {
        return _transitions
            .Where(kvp => kvp.Key.Item1 == _order.State)
            .Select(kvp => kvp.Key.Item2);
    }
}

Q: Implement order reconciliation to detect missing confirmations.

A: Compare internal orders with broker confirmations.

public class OrderReconciliationService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<OrderReconciliationService> _logger;
    private readonly TimeSpan _reconciliationInterval = TimeSpan.FromMinutes(5);
    private readonly TimeSpan _confirmationTimeout = TimeSpan.FromMinutes(2);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ReconcileOrdersAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Order reconciliation failed");
            }

            await Task.Delay(_reconciliationInterval, stoppingToken);
        }
    }

    private async Task ReconcileOrdersAsync(CancellationToken ct)
    {
        using var scope = _serviceProvider.CreateScope();
        var orderRepo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        var mt5Gateway = scope.ServiceProvider.GetRequiredService<IMt5Gateway>();

        // Find orders awaiting confirmation
        var pendingOrders = await orderRepo.GetPendingConfirmationsAsync(
            olderThan: DateTime.UtcNow - _confirmationTimeout,
            ct);

        _logger.LogInformation("Reconciling {Count} pending orders", pendingOrders.Count);

        foreach (var order in pendingOrders)
        {
            try
            {
                // Query broker for order status
                var brokerOrder = await mt5Gateway.GetOrderByClientIdAsync(order.ClientOrderId, ct);

                if (brokerOrder != null)
                {
                    // Order found at broker - update status
                    await SyncOrderStatusAsync(order, brokerOrder, orderRepo, ct);
                }
                else
                {
                    // Order not found - may have been rejected or lost
                    await HandleMissingOrderAsync(order, orderRepo, ct);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Failed to reconcile order {OrderId}",
                    order.Id);
            }
        }
    }

    private async Task SyncOrderStatusAsync(
        Order internalOrder,
        BrokerOrder brokerOrder,
        IOrderRepository orderRepo,
        CancellationToken ct)
    {
        if (internalOrder.State != MapBrokerState(brokerOrder.State))
        {
            _logger.LogWarning(
                "Order {OrderId} state mismatch. Internal: {InternalState}, Broker: {BrokerState}",
                internalOrder.Id,
                internalOrder.State,
                brokerOrder.State);

            internalOrder.State = MapBrokerState(brokerOrder.State);
            internalOrder.BrokerOrderId = brokerOrder.OrderId;
            internalOrder.FilledVolume = brokerOrder.FilledVolume;
            internalOrder.AveragePrice = brokerOrder.AveragePrice;

            await orderRepo.UpdateAsync(internalOrder, ct);
        }
    }

    private async Task HandleMissingOrderAsync(
        Order order,
        IOrderRepository orderRepo,
        CancellationToken ct)
    {
        _logger.LogError(
            "Order {OrderId} not found at broker after {Minutes} minutes",
            order.Id,
            _confirmationTimeout.TotalMinutes);

        // Mark as potentially lost
        order.State = OrderState.Rejected;
        order.RejectReason = "Order not confirmed by broker - may be lost";
        await orderRepo.UpdateAsync(order, ct);

        // Trigger alert for manual investigation
        // await _alertService.SendAlertAsync(...)
    }

    private OrderState MapBrokerState(string brokerState)
    {
        return brokerState switch
        {
            "PENDING" => OrderState.Submitted,
            "PARTIAL" => OrderState.PartiallyFilled,
            "FILLED" => OrderState.Filled,
            "CANCELLED" => OrderState.Cancelled,
            "REJECTED" => OrderState.Rejected,
            _ => OrderState.Pending
        };
    }
}

---

MT4/MT5 Integration

Q: Implement MT5 Gateway with connection pooling and failover.

A: Manage multiple MT5 connections for reliability.

public interface IMt5Gateway
{
    Task<string> SendOrderAsync(OrderRequest request, CancellationToken ct = default);
    Task<BrokerOrder> GetOrderByClientIdAsync(string clientOrderId, CancellationToken ct = default);
    Task<bool> CancelOrderAsync(string orderId, CancellationToken ct = default);
}

public class Mt5ConnectionPool
{
    private readonly List<Mt5Connection> _connections = new();
    private readonly SemaphoreSlim _lock = new(1, 1);
    private readonly IConfiguration _configuration;
    private readonly ILogger<Mt5ConnectionPool> _logger;
    private int _currentIndex = 0;

    public Mt5ConnectionPool(IConfiguration configuration, ILogger<Mt5ConnectionPool> logger)
    {
        _configuration = configuration;
        _logger = logger;
    }

    public async Task InitializeAsync()
    {
        var servers = _configuration.GetSection("Mt5:Servers").Get<List<Mt5ServerConfig>>();

        foreach (var server in servers)
        {
            try
            {
                var connection = new Mt5Connection(server);
                await connection.ConnectAsync();
                _connections.Add(connection);

                _logger.LogInformation(
                    "Connected to MT5 server: {Server}:{Port}",
                    server.Host,
                    server.Port);
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Failed to connect to MT5 server: {Server}:{Port}",
                    server.Host,
                    server.Port);
            }
        }

        if (_connections.Count == 0)
        {
            throw new InvalidOperationException("No MT5 connections available");
        }
    }

    public async Task<Mt5Connection> GetConnectionAsync()
    {
        await _lock.WaitAsync();
        try
        {
            // Round-robin with health check
            for (int i = 0; i < _connections.Count; i++)
            {
                var index = (_currentIndex + i) % _connections.Count;
                var connection = _connections[index];

                if (await connection.IsHealthyAsync())
                {
                    _currentIndex = (index + 1) % _connections.Count;
                    return connection;
                }
            }

            throw new InvalidOperationException("No healthy MT5 connections available");
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task<T> ExecuteWithRetryAsync<T>(
        Func<Mt5Connection, Task<T>> operation,
        int maxRetries = 3)
    {
        Exception lastException = null;

        for (int attempt = 0; attempt < maxRetries; attempt++)
        {
            try
            {
                var connection = await GetConnectionAsync();
                return await operation(connection);
            }
            catch (Mt5ConnectionException ex)
            {
                lastException = ex;
                _logger.LogWarning(
                    ex,
                    "MT5 operation failed (attempt {Attempt}/{MaxRetries})",
                    attempt + 1,
                    maxRetries);

                if (attempt < maxRetries - 1)
                {
                    await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)));
                }
            }
        }

        throw new Mt5GatewayException(
            $"MT5 operation failed after {maxRetries} attempts",
            lastException);
    }
}

public class Mt5Gateway : IMt5Gateway
{
    private readonly Mt5ConnectionPool _connectionPool;
    private readonly ILogger<Mt5Gateway> _logger;

    public Mt5Gateway(Mt5ConnectionPool connectionPool, ILogger<Mt5Gateway> logger)
    {
        _connectionPool = connectionPool;
        _logger = logger;
    }

    public async Task<string> SendOrderAsync(OrderRequest request, CancellationToken ct = default)
    {
        return await _connectionPool.ExecuteWithRetryAsync(async connection =>
        {
            var mt5Order = new Mt5OrderRequest
            {
                Symbol = request.Symbol,
                Volume = request.Volume,
                Type = MapOrderType(request.Type),
                Price = request.Price,
                StopLoss = request.StopLoss,
                TakeProfit = request.TakeProfit,
                Comment = request.ClientOrderId,
                Magic = CalculateMagicNumber(request.AccountId)
            };

            var result = await connection.SendOrderAsync(mt5Order, ct);

            if (result.ReturnCode != Mt5ReturnCode.Success)
            {
                throw new Mt5OrderException(
                    $"Order rejected: {result.ReturnCode} - {result.Comment}");
            }

            _logger.LogInformation(
                "Order sent to MT5. ClientOrderId: {ClientOrderId}, Ticket: {Ticket}",
                request.ClientOrderId,
                result.OrderTicket);

            return result.OrderTicket.ToString();
        });
    }

    public async Task<BrokerOrder> GetOrderByClientIdAsync(
        string clientOrderId,
        CancellationToken ct = default)
    {
        return await _connectionPool.ExecuteWithRetryAsync(async connection =>
        {
            // Query orders by comment (where we store client order ID)
            var orders = await connection.GetOrdersAsync(comment: clientOrderId, ct);
            return orders.FirstOrDefault();
        });
    }

    public async Task<bool> CancelOrderAsync(string orderId, CancellationToken ct = default)
    {
        return await _connectionPool.ExecuteWithRetryAsync(async connection =>
        {
            var result = await connection.CancelOrderAsync(long.Parse(orderId), ct);
            return result.ReturnCode == Mt5ReturnCode.Success;
        });
    }

    private Mt5OrderType MapOrderType(OrderType type)
    {
        return type switch
        {
            OrderType.Market => Mt5OrderType.Market,
            OrderType.Limit => Mt5OrderType.Limit,
            OrderType.Stop => Mt5OrderType.Stop,
            OrderType.StopLimit => Mt5OrderType.StopLimit,
            _ => throw new ArgumentException($"Unsupported order type: {type}")
        };
    }

    private int CalculateMagicNumber(Guid accountId)
    {
        // Generate deterministic magic number from account ID
        return Math.Abs(accountId.GetHashCode() % 999999);
    }
}

Q: Handle MT5 error codes and map to domain exceptions.

A: Translate MT5-specific errors to business errors.

public enum Mt5ReturnCode
{
    Success = 10009,
    InvalidPrice = 10015,
    InvalidStops = 10016,
    InvalidVolume = 10014,
    MarketClosed = 10018,
    NoMoney = 10019,
    PriceChanged = 10020,
    Requote = 10004,
    Timeout = 10024,
    TooManyRequests = 10025,
    TradeDisabled = 10017,
    ConnectionError = 10030
}

public class Mt5ErrorMapper
{
    private static readonly Dictionary<Mt5ReturnCode, (string Code, string Message)> _errorMap = new()
    {
        { Mt5ReturnCode.InvalidPrice, ("INVALID_PRICE", "Order price is invalid or too far from market") },
        { Mt5ReturnCode.InvalidStops, ("INVALID_STOPS", "Stop loss or take profit is invalid") },
        { Mt5ReturnCode.InvalidVolume, ("INVALID_VOLUME", "Order volume is invalid") },
        { Mt5ReturnCode.MarketClosed, ("MARKET_CLOSED", "Market is closed") },
        { Mt5ReturnCode.NoMoney, ("INSUFFICIENT_MARGIN", "Insufficient margin to open position") },
        { Mt5ReturnCode.PriceChanged, ("PRICE_CHANGED", "Price changed, retry with updated price") },
        { Mt5ReturnCode.Requote, ("REQUOTE", "Broker requote received") },
        { Mt5ReturnCode.Timeout, ("TIMEOUT", "Order execution timeout") },
        { Mt5ReturnCode.TooManyRequests, ("RATE_LIMITED", "Too many requests to broker") },
        { Mt5ReturnCode.TradeDisabled, ("TRADE_DISABLED", "Trading is disabled for this symbol") },
        { Mt5ReturnCode.ConnectionError, ("CONNECTION_ERROR", "Connection to broker lost") }
    };

    public static OrderException MapToException(Mt5ReturnCode returnCode, string additionalInfo = null)
    {
        if (_errorMap.TryGetValue(returnCode, out var error))
        {
            var message = additionalInfo != null
                ? $"{error.Message}. {additionalInfo}"
                : error.Message;

            return new OrderException(error.Code, message)
            {
                IsRetryable = IsRetryable(returnCode),
                RequiresClientAction = RequiresClientAction(returnCode)
            };
        }

        return new OrderException(
            "BROKER_ERROR",
            $"Unknown broker error: {returnCode}")
        {
            IsRetryable = false
        };
    }

    private static bool IsRetryable(Mt5ReturnCode returnCode)
    {
        return returnCode switch
        {
            Mt5ReturnCode.PriceChanged => true,
            Mt5ReturnCode.Requote => true,
            Mt5ReturnCode.Timeout => true,
            Mt5ReturnCode.ConnectionError => true,
            _ => false
        };
    }

    private static bool RequiresClientAction(Mt5ReturnCode returnCode)
    {
        return returnCode switch
        {
            Mt5ReturnCode.NoMoney => true,
            Mt5ReturnCode.InvalidPrice => true,
            Mt5ReturnCode.InvalidStops => true,
            Mt5ReturnCode.InvalidVolume => true,
            Mt5ReturnCode.TradeDisabled => true,
            _ => false
        };
    }
}

public class OrderException : Exception
{
    public string ErrorCode { get; }
    public bool IsRetryable { get; init; }
    public bool RequiresClientAction { get; init; }

    public OrderException(string errorCode, string message) : base(message)
    {
        ErrorCode = errorCode;
    }
}

---

Market Data Processing

Q: Implement high-frequency tick data processor with conflation.

A: Process and aggregate tick updates efficiently.

public class TickDataProcessor
{
    private readonly Channel<Tick> _inboundChannel;
    private readonly IDistributedCache _cache;
    private readonly IHubContext<PriceHub> _hubContext;
    private readonly ILogger<TickDataProcessor> _logger;

    private readonly Dictionary<string, TickAggregator> _aggregators = new();
    private readonly TimeSpan _conflationWindow = TimeSpan.FromMilliseconds(100);

    public TickDataProcessor(
        IDistributedCache cache,
        IHubContext<PriceHub> hubContext,
        ILogger<TickDataProcessor> logger)
    {
        _inboundChannel = Channel.CreateBounded<Tick>(new BoundedChannelOptions(10000)
        {
            FullMode = BoundedChannelFullMode.DropOldest
        });
        _cache = cache;
        _hubContext = hubContext;
        _logger = logger;
    }

    public async Task ProcessTickAsync(Tick tick)
    {
        await _inboundChannel.Writer.WriteAsync(tick);
    }

    public async Task StartProcessingAsync(CancellationToken ct)
    {
        var processingTasks = Enumerable.Range(0, Environment.ProcessorCount)
            .Select(i => ProcessTicksAsync(ct))
            .ToArray();

        // Periodic flush for low-frequency symbols
        var flushTask = PeriodicFlushAsync(ct);

        await Task.WhenAll(processingTasks.Concat(new[] { flushTask }));
    }

    private async Task ProcessTicksAsync(CancellationToken ct)
    {
        await foreach (var tick in _inboundChannel.Reader.ReadAllAsync(ct))
        {
            try
            {
                if (!_aggregators.TryGetValue(tick.Symbol, out var aggregator))
                {
                    aggregator = new TickAggregator(tick.Symbol, _conflationWindow);
                    _aggregators[tick.Symbol] = aggregator;
                }

                var conflatedTick = aggregator.AddTick(tick);

                if (conflatedTick != null)
                {
                    await PublishTickAsync(conflatedTick, ct);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing tick for {Symbol}", tick.Symbol);
            }
        }
    }

    private async Task PeriodicFlushAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(_conflationWindow);

        while (await timer.WaitForNextTickAsync(ct))
        {
            foreach (var aggregator in _aggregators.Values)
            {
                var tick = aggregator.Flush();
                if (tick != null)
                {
                    await PublishTickAsync(tick, ct);
                }
            }
        }
    }

    private async Task PublishTickAsync(Tick tick, CancellationToken ct)
    {
        // Update cache
        var cacheKey = $"price:{tick.Symbol}";
        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(tick),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            },
            ct);

        // Broadcast to WebSocket clients
        await _hubContext.Clients.Group(tick.Symbol)
            .SendAsync("price", tick, ct);

        _logger.LogDebug(
            "Published tick: {Symbol} Bid={Bid} Ask={Ask}",
            tick.Symbol,
            tick.Bid,
            tick.Ask);
    }
}

public class TickAggregator
{
    private readonly string _symbol;
    private readonly TimeSpan _window;
    private Tick _pendingTick;
    private DateTime _windowStart;

    public TickAggregator(string symbol, TimeSpan window)
    {
        _symbol = symbol;
        _window = window;
        _windowStart = DateTime.UtcNow;
    }

    public Tick AddTick(Tick tick)
    {
        var now = DateTime.UtcNow;

        if (_pendingTick == null)
        {
            _pendingTick = tick;
            _windowStart = now;
            return null;
        }

        // Aggregate tick data
        _pendingTick = new Tick
        {
            Symbol = _symbol,
            Bid = tick.Bid,  // Latest bid
            Ask = tick.Ask,  // Latest ask
            BidVolume = _pendingTick.BidVolume + tick.BidVolume,  // Sum volumes
            AskVolume = _pendingTick.AskVolume + tick.AskVolume,
            Timestamp = tick.Timestamp
        };

        // Check if window elapsed
        if (now - _windowStart >= _window)
        {
            var result = _pendingTick;
            _pendingTick = null;
            return result;
        }

        return null;
    }

    public Tick Flush()
    {
        var result = _pendingTick;
        _pendingTick = null;
        return result;
    }
}

Q: Implement VWAP (Volume-Weighted Average Price) calculator.

A: Calculate real-time VWAP from tick stream.

public class VwapCalculator
{
    private readonly Dictionary<string, VwapData> _vwapBySymbol = new();
    private readonly TimeSpan _window;

    public VwapCalculator(TimeSpan window)
    {
        _window = window;
    }

    public decimal CalculateVwap(string symbol, decimal price, decimal volume, DateTime timestamp)
    {
        if (!_vwapBySymbol.TryGetValue(symbol, out var data))
        {
            data = new VwapData();
            _vwapBySymbol[symbol] = data;
        }

        // Add new trade
        data.Trades.Add(new Trade
        {
            Price = price,
            Volume = volume,
            Timestamp = timestamp
        });

        // Remove trades outside window
        var cutoff = timestamp - _window;
        data.Trades.RemoveAll(t => t.Timestamp < cutoff);

        // Calculate VWAP
        if (data.Trades.Count == 0)
            return 0;

        var totalValue = data.Trades.Sum(t => t.Price * t.Volume);
        var totalVolume = data.Trades.Sum(t => t.Volume);

        return totalVolume > 0 ? totalValue / totalVolume : 0;
    }

    private class VwapData
    {
        public List<Trade> Trades { get; } = new();
    }

    private class Trade
    {
        public decimal Price { get; set; }
        public decimal Volume { get; set; }
        public DateTime Timestamp { get; set; }
    }
}

// Usage in tick processor
public class EnhancedTickProcessor
{
    private readonly VwapCalculator _vwapCalculator;

    public async Task ProcessTradeAsync(Trade trade)
    {
        var vwap = _vwapCalculator.CalculateVwap(
            trade.Symbol,
            trade.Price,
            trade.Volume,
            trade.Timestamp);

        await PublishMarketDataAsync(new MarketData
        {
            Symbol = trade.Symbol,
            LastPrice = trade.Price,
            Vwap = vwap,
            Timestamp = trade.Timestamp
        });
    }
}

---

Risk Management

Q: Implement real-time P&L calculator for open positions.

A: Calculate unrealized profit/loss continuously.

public class PnLCalculator
{
    private readonly IMarketDataService _marketData;
    private readonly ILogger<PnLCalculator> _logger;

    public async Task<PositionPnL> CalculatePnLAsync(Position position, CancellationToken ct = default)
    {
        var currentPrice = await _marketData.GetPriceAsync(position.Symbol, ct);

        var closePrice = position.Type == PositionType.Long
            ? currentPrice.Bid  // Close long at bid
            : currentPrice.Ask;  // Close short at ask

        // Calculate P&L in position currency
        decimal pnl;
        if (position.Type == PositionType.Long)
        {
            pnl = (closePrice - position.OpenPrice) * position.Volume;
        }
        else
        {
            pnl = (position.OpenPrice - closePrice) * position.Volume;
        }

        // Apply contract size
        pnl *= position.ContractSize;

        // Convert to account currency if needed
        if (position.Currency != position.AccountCurrency)
        {
            var conversionRate = await GetConversionRateAsync(
                position.Currency,
                position.AccountCurrency,
                ct);
            pnl *= conversionRate;
        }

        // Calculate swap (overnight financing)
        var swap = CalculateSwap(position);

        // Calculate commission
        var commission = CalculateCommission(position);

        var netPnl = pnl + swap - commission;

        return new PositionPnL
        {
            PositionId = position.Id,
            GrossPnL = pnl,
            Swap = swap,
            Commission = commission,
            NetPnL = netPnl,
            PnLPercentage = (netPnl / (position.OpenPrice * position.Volume * position.ContractSize)) * 100,
            CurrentPrice = closePrice,
            Timestamp = DateTime.UtcNow
        };
    }

    public async Task<AccountPnL> CalculateAccountPnLAsync(
        Guid accountId,
        CancellationToken ct = default)
    {
        var positions = await GetOpenPositionsAsync(accountId, ct);

        var pnlTasks = positions.Select(p => CalculatePnLAsync(p, ct));
        var pnls = await Task.WhenAll(pnlTasks);

        return new AccountPnL
        {
            AccountId = accountId,
            TotalGrossPnL = pnls.Sum(p => p.GrossPnL),
            TotalSwap = pnls.Sum(p => p.Swap),
            TotalCommission = pnls.Sum(p => p.Commission),
            TotalNetPnL = pnls.Sum(p => p.NetPnL),
            PositionCount = positions.Count,
            Positions = pnls.ToList(),
            Timestamp = DateTime.UtcNow
        };
    }

    private decimal CalculateSwap(Position position)
    {
        var days = (DateTime.UtcNow - position.OpenTime).Days;
        if (days == 0) return 0;

        // Simplified swap calculation
        var swapRate = position.Type == PositionType.Long
            ? position.SwapLong
            : position.SwapShort;

        return swapRate * position.Volume * days;
    }

    private decimal CalculateCommission(Position position)
    {
        // Commission charged on open
        return position.Commission;
    }

    private async Task<decimal> GetConversionRateAsync(
        string fromCurrency,
        string toCurrency,
        CancellationToken ct)
    {
        if (fromCurrency == toCurrency)
            return 1;

        var symbol = $"{fromCurrency}{toCurrency}";
        var price = await _marketData.GetPriceAsync(symbol, ct);

        return (price.Bid + price.Ask) / 2;
    }
}

Q: Implement margin calculator with different leverage levels.

A: Calculate required margin for positions.

public class MarginCalculator
{
    private readonly ISymbolConfigService _symbolConfig;

    public async Task<decimal> CalculateRequiredMarginAsync(
        string symbol,
        decimal volume,
        int leverage,
        CancellationToken ct = default)
    {
        var config = await _symbolConfig.GetConfigAsync(symbol, ct);

        // Get current market price
        var price = await GetMarketPriceAsync(symbol, ct);

        // Calculate position value
        var positionValue = volume * config.ContractSize * price;

        // Apply leverage
        var requiredMargin = positionValue / leverage;

        // Apply margin requirements (can vary by symbol, time, volatility)
        var marginMultiplier = await GetMarginMultiplierAsync(symbol, ct);
        requiredMargin *= marginMultiplier;

        return requiredMargin;
    }

    public async Task<MarginStatus> CalculateMarginStatusAsync(
        Account account,
        List<Position> positions,
        CancellationToken ct = default)
    {
        // Calculate used margin
        var usedMarginTasks = positions.Select(async p =>
            await CalculateRequiredMarginAsync(p.Symbol, p.Volume, p.Leverage, ct));

        var usedMargins = await Task.WhenAll(usedMarginTasks);
        var totalUsedMargin = usedMargins.Sum();

        // Calculate unrealized P&L
        var pnlCalculator = new PnLCalculator(_marketDataService, _logger);
        var accountPnl = await pnlCalculator.CalculateAccountPnLAsync(account.Id, ct);

        // Calculate equity and free margin
        var equity = account.Balance + accountPnl.TotalNetPnL;
        var freeMargin = equity - totalUsedMargin;
        var marginLevel = totalUsedMargin > 0 ? (equity / totalUsedMargin) * 100 : 0;

        return new MarginStatus
        {
            Balance = account.Balance,
            Equity = equity,
            UsedMargin = totalUsedMargin,
            FreeMargin = freeMargin,
            MarginLevel = marginLevel,
            UnrealizedPnL = accountPnl.TotalNetPnL,
            IsMarginCall = marginLevel > 0 && marginLevel <= account.MarginCallLevel,
            IsStopOut = marginLevel > 0 && marginLevel <= account.StopOutLevel
        };
    }

    private async Task<decimal> GetMarginMultiplierAsync(string symbol, CancellationToken ct)
    {
        // Margin requirements can increase during:
        // - High volatility periods
        // - Weekend/overnight
        // - Major news events
        // - Low liquidity

        var config = await _symbolConfig.GetConfigAsync(symbol, ct);

        decimal multiplier = 1.0m;

        // Weekend margin (typically higher)
        if (IsWeekend())
        {
            multiplier *= config.WeekendMarginMultiplier;
        }

        // Overnight margin
        if (IsOvernight())
        {
            multiplier *= config.OvernightMarginMultiplier;
        }

        // Volatility adjustment
        var volatility = await GetCurrentVolatilityAsync(symbol, ct);
        if (volatility > config.HighVolatilityThreshold)
        {
            multiplier *= config.HighVolatilityMarginMultiplier;
        }

        return multiplier;
    }

    private bool IsWeekend()
    {
        var now = DateTime.UtcNow;
        return now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday;
    }

    private bool IsOvernight()
    {
        var now = DateTime.UtcNow.TimeOfDay;
        return now < TimeSpan.FromHours(8) || now > TimeSpan.FromHours(22);
    }
}

Q: Implement automatic stop-out mechanism.

A: Close positions when margin level falls below threshold.

public class StopOutService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<StopOutService> _logger;
    private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await CheckStopOutLevelsAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error checking stop-out levels");
            }

            await Task.Delay(_checkInterval, stoppingToken);
        }
    }

    private async Task CheckStopOutLevelsAsync(CancellationToken ct)
    {
        using var scope = _serviceProvider.CreateScope();
        var accountRepo = scope.ServiceProvider.GetRequiredService<IAccountRepository>();
        var positionRepo = scope.ServiceProvider.GetRequiredService<IPositionRepository>();
        var marginCalc = scope.ServiceProvider.GetRequiredService<MarginCalculator>();
        var tradingService = scope.ServiceProvider.GetRequiredService<ITradingService>();

        // Get accounts with open positions
        var accounts = await accountRepo.GetAccountsWithPositionsAsync(ct);

        foreach (var account in accounts)
        {
            var positions = await positionRepo.GetOpenPositionsAsync(account.Id, ct);

            if (positions.Count == 0)
                continue;

            var marginStatus = await marginCalc.CalculateMarginStatusAsync(
                account,
                positions,
                ct);

            if (marginStatus.IsStopOut)
            {
                _logger.LogWarning(
                    "Stop-out triggered for account {AccountId}. Margin level: {MarginLevel}%",
                    account.Id,
                    marginStatus.MarginLevel);

                await ExecuteStopOutAsync(account, positions, marginStatus, tradingService, ct);
            }
            else if (marginStatus.IsMarginCall)
            {
                _logger.LogWarning(
                    "Margin call for account {AccountId}. Margin level: {MarginLevel}%",
                    account.Id,
                    marginStatus.MarginLevel);

                await SendMarginCallNotificationAsync(account, marginStatus, ct);
            }
        }
    }

    private async Task ExecuteStopOutAsync(
        Account account,
        List<Position> positions,
        MarginStatus marginStatus,
        ITradingService tradingService,
        CancellationToken ct)
    {
        // Close positions starting with largest losing position
        var positionsByLoss = positions
            .OrderBy(p => p.UnrealizedPnL)
            .ToList();

        foreach (var position in positionsByLoss)
        {
            try
            {
                await tradingService.ClosePositionAsync(
                    position.Id,
                    reason: "Stop-out",
                    ct);

                _logger.LogInformation(
                    "Closed position {PositionId} due to stop-out. P&L: {PnL}",
                    position.Id,
                    position.UnrealizedPnL);

                // Recalculate margin status
                var remainingPositions = positions.Where(p => p.Id != position.Id).ToList();
                if (remainingPositions.Count == 0)
                    break;

                marginStatus = await _marginCalc.CalculateMarginStatusAsync(
                    account,
                    remainingPositions,
                    ct);

                // Stop if margin level recovered
                if (!marginStatus.IsStopOut)
                    break;
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Failed to close position {PositionId} during stop-out",
                    position.Id);
            }
        }
    }
}

---

Advanced Trading Scenarios

Q: Explain slippage and how you would measure it.

A: Slippage is the difference between expected and executed price. Measure average slippage per symbol and market condition, and alert when it exceeds thresholds.

Q: How do you handle market data bursts without dropping critical updates?

A: Use conflation (latest per symbol), prioritize high-value symbols, and stream snapshots plus deltas.

Q: Design an order book snapshot + delta model.

A: Publish a full snapshot periodically and send incremental updates in between, keyed by sequence numbers for replay and gap detection.

Q: Implement a real-time PnL calculation at the account level.

A: Aggregate position PnL plus realized PnL; update on price ticks and trade fills.

Q: Describe a reconciliation workflow for executed trades.

A: Compare internal fills to broker confirmations, identify mismatches, and run compensating adjustments with audit logs.

---

Total Exercises: 30+

Master trading domain concepts for building robust, compliant trading platforms!